#!/usr/bin/python3
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t -*-

import os
import lxml
import json
import subprocess
from wrt_util import WrtUtil
from wrt_param import WrtConst


class WrtPluginHub:

    def __init__(self):
        self.pluginsDir = os.path.join(WrtConst.libDir, "plugins")

        self._pluginList = []           # FIXME: should be dict, boost performance
        self._preloadAllPlugins()

    def dispose(self):
        for obj in self._pluginList:
            assert obj.is_loaded() is None

    def getPluginById(self, pluginId):
        assert pluginId is not None

        for obj in self._pluginList:
            if obj._id == pluginId:
                return obj
        assert False

    def getConnectionPluginList(self):
        return [x for x in self._pluginList if x._pluginType == WrtConst.PLUGIN_TYPE_CONNECTION]

    def getPrivnetPluginList(self):
        return [x for x in self._pluginList if x._pluginType == WrtConst.PLUGIN_TYPE_PRIVNET]

    def getSubnetl2PluginList(self):
        return [x for x in self._pluginList if x._pluginType == WrtConst.PLUGIN_TYPE_SUBNET_L2]

    def getSubnetl3PluginList(self):
        return [x for x in self._pluginList if x._pluginType == WrtConst.PLUGIN_TYPE_SUBNET_L3]

    def getNtpAgentPluginList(self):
        assert False

    def getWinsAgentPluginList(self):
        assert False

    def getSelectorPlugin(self):
        ret = [x for x in self._pluginList if x._pluginType == WrtConst.PLUGIN_TYPE_SELECTOR]
        if len(ret) == 0:
            return None
        else:
            return ret[0]

    def notifyCfgChangeByType(self, pluginType, newConfig):
        pass

    def notifyEtcDirChange(self):
        pass

    def _preloadAllPlugins(self):
        for fn in os.listdir(self.pluginsDir):
            self.__preloadPlugin(fn)

    def __preloadPlugin(self, pluginName):
        path = os.path.join(self.pluginsDir, pluginName)

        # get metadata.xml file
        metadata_file = os.path.join(path, "metadata.xml")
        if not os.path.exists(metadata_file):
            raise Exception("plugin %s has no metadata.xml" % (pluginName))
        if not os.path.isfile(metadata_file):
            raise Exception("metadata.xml for plugin %s is not a file" % (pluginName))
        if not os.access(metadata_file, os.R_OK):
            raise Exception("metadata.xml for plugin %s is invalid" % (pluginName))

        # check metadata.xml file content
        # FIXME
        rootElem = lxml.etree.parse(metadata_file).getroot()
        # if True:
        #     dtd = libxml2.parseDTD(None, constants.PATH_PLUGIN_DTD_FILE)
        #     ctxt = libxml2.newValidCtxt()
        #     messages = []
        #     ctxt.setValidityErrorHandler(lambda item, msgs: msgs.append(item), None, messages)
        #     if tree.validateDtd(ctxt, dtd) != 1:
        #         msg = ""
        #         for i in messages:
        #             msg += i
        #         raise exceptions.IncorrectPluginMetaFile(metadata_file, msg)

        if rootElem.tag == "plugins":
            for child in rootElem.xpath("./plugin"):
                childId = child.get("id")
                self._pluginList.append(_Plugin.new(childId, path, child))
        elif rootElem.tag == "plugin":
            pluginId = rootElem.get("id")
            self._pluginList.append(_Plugin.new(pluginId, path, rootElem))
        else:
            raise Exception("invalid root tag for plugin with name \"%s\"" % (pluginName))


class _Plugin:

    @staticmethod
    def new(id, path, rootElem):
        if len(rootElem.xpath("./type")) > 0:
            pluginTypeStr = rootElem.xpath("./description")[0].text
            if pluginTypeStr == "connection":
                return _ConnectionPlugin(id, path, rootElem)
            elif pluginTypeStr == "privnet":
                return _PrivnetPlugin(id, path, rootElem)
            elif pluginTypeStr == "subnetl2":
                return _Subnetl2Plugin(id, path, rootElem)
            elif pluginTypeStr == "subnetl3":
                return _Subnetl3Plugin(id, path, rootElem)
            elif pluginTypeStr == "selector":
                return _SelectorPlugin(id, path, rootElem)
            else:
                raise Exception("invalid type property \"%s\" in plugin with id \"%s\"" % (pluginTypeStr, id))
        else:
            raise Exception("no type property in plugin with id \"%s\"" % (id))

    def __init__(self, id, pluginType, path, rootElem):
        self._id = id
        self._pluginType = pluginType
        self._path = path
        self._desc = None
        self._bNeedTmpDir = False
        self._bNeedDataDir = False

        # get description property
        if len(rootElem.xpath("./description")) > 0:
            self._desc = rootElem.xpath("./description")[0].text
        else:
            raise Exception("no description property in plugin with id \"%s\"" % (id))

        # get need-temp-directory property
        if len(rootElem.xpath("./need-temp-directory")) > 0:
            self._bNeedTmpDir = True

        # get need-data-directory property
        if len(rootElem.xpath("./need-data-directory")) > 0:
            self._bNeedDataDir = True

    @property
    def id(self):
        return self._id

    @property
    def plugin_type(self):
        return self._pluginType

    @property
    def description(self):
        return self._desc

    def is_loaded(self):
        raise NotImplementedError()


class _ObjectFormPlugin(_Plugin):

    def __init__(self, id, pluginType, path, rootElem):
        super(self).__init__(id, pluginType, path, rootElem)

        self._fileName = None
        self._className = None
        self._workerObj = None

        # get filename property
        if len(rootElem.xpath("./filename")) > 0:
            self._fileName = rootElem.xpath("./filename").text
        else:
            raise Exception("no filename property in plugin with id \"%s\"" % (id))

        # get classname property
        if len(rootElem.xpath("./classname")) > 0:
            self._className = rootElem.xpath("./classname").text
        else:
            raise Exception("no classname property in plugin with id \"%s\"" % (id))

        # plugin type specific processing
        if self._pluginType == WrtConst.PLUGIN_TYPE_PRIVNET:
            self.__parsePrivnetPlugin()
        elif self._pluginType == WrtConst.PLUGIN_TYPE_SUBNET:
            self.__parseSubNetPlugin()
        else:
            assert False

    def is_loaded(self):
        return self._workerObj is not None

    def load(self):
        assert self._workerObj is None
        modname, mod = WrtUtil.loadPythonFile(os.path.join(self._path, self._fileName))
        self._workerObj = exec("mod.%s" % (self._className))

    def unload(self):
        if self._workerObj is not None:
            self._workerObj.dispose()
            self._workerObj = None

    def __parsePrivnetPlugin(self):
        pass

    def __parseSubnetPlugin(self):
        pass

    def __getattr__(self, attr):
        if self._workerObj is None or attr.startswith("_"):
            raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, attr))
        else:
            return getattr(self._workerObj, attr)


class _ProcessFormPlugin(_Plugin):

    def __init__(self, id, pluginType, path, rootElem):
        super(self).__init__(id, pluginType, path, rootElem)

        self._execFile = None
        self._proc = None

        # get executable property
        if len(rootElem.xpath("./executable")) > 0:
            self._execFile = rootElem.xpath("./executable").text
        else:
            raise Exception("no executable property in plugin with id \"%s\"" % (id))

    def is_loaded(self):
        return self._proc is not None

    def run(self, param):
        assert self._proc is not None
        fullfn = os.path.join(self._path, self._execFile)
        self._proc = subprocess.Popen([fullfn, json.dumps(param)])

    def terminate(self):
        if self._proc is not None:
            self._proc.terminate()
            self._proc.wait()
            self._proc = None


class _ConnectionPlugin(_ProcessFormPlugin):

    def __init__(self, id, path, rootElem):
        super(self).__init__(id, WrtConst.PLUGIN_TYPE_CONNECTION, path, rootElem)

        self._level = None
        self._bNeedNet = False

        # get level property
        if len(rootElem.xpath("./level")) > 0:
            self._level = rootElem.xpath("./level").text
            if self._level not in ["1", "2"]:
                raise Exception("invalid level property in plugin with id \"%s\"" % (id))
        else:
            raise Exception("no level property in plugin with id \"%s\"" % (id))

        # get network-access property
        if len(rootElem.xpath("./network-access")) > 0:
            self._bNeedNet = True

    @property
    def level(self):
        return self._level


class _PrivnetPlugin(_ObjectFormPlugin):

    def __init__(self, id, path, rootElem):
        super(self).__init__(id, WrtConst.PLUGIN_TYPE_PRIVNET, path, rootElem)


class _Subnetl2Plugin(_ObjectFormPlugin):

    def __init__(self, id, path, rootElem):
        super(self).__init__(id, WrtConst.PLUGIN_TYPE_SUBNET_L2, path, rootElem)


class _Subnetl3Plugin(_ObjectFormPlugin):

    def __init__(self, id, path, rootElem):
        super(self).__init__(id, WrtConst.PLUGIN_TYPE_SUBNET_L3, path, rootElem)


class _SelectorPlugin(_ObjectFormPlugin):

    def __init__(self, id, path, rootElem):
        super(self).__init__(id, WrtConst.PLUGIN_TYPE_SELECTOR, path, rootElem)
